Back to list
第 5 章:線形変換と Softmax
Chapter 5: Linear Transformation and Softmax
Translated: 2026/4/25 0:00:23
Japanese Translation
作成する機能 2 つ:ニューラルネットワークのほぼすべての層に見られるヘルパー関数です。
Linear:入力ベクトルと重み行列を受け取り、重みの各要素を入力と要素毎に掛け合わせ、各行を 1 つの出力値に合計します。例:
入力:[1, 2, 3]
重み:[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
行 0: 0.1*1 + 0.2*2 + 0.3*3 = 1.4
行 1: 0.4*1 + 0.5*2 + 0.6*3 = 3.2
出力:[1.4, 3.2]
重みが 2 行の出力を持つと、これはニューラルネットワークがデータを通過する際にどのようにサイズを変更するのでしょうか。Softmax:生の数字のリストを受け取り、合計が 1 になる確率分布に変換します。例えば、[2.0, 1.0, 0.1] は約 [0.66, 0.24, 0.10] になります。最大の入力が最大の確率を得ます。彼らは独自のファイルにあります、それは純粋な数学のユーティリティであり、モデルアーキテクチャに依存しません。第 1 章~2 章(Value クラス - コンマレートの記録者)。Linear には 2 つの Value オブジェクトのリスト間のドット積を計算する手段が必要です。このメソッドを Value.cs に追加します:
// --- Value.cs(Value クラス内)---
public static Value Dot(List a, List b) {
var result = new Value(0);
for (int i = 0; i < a.Count; i++) {
result += a[i] * b[i];
}
return result;
}
ここではから Debug と Release が重要になります。各 += は新しい Value をアロケートし、通常のトレーニングステップでは数千個です。Debug モードでは JIT が内線しをスキップし GC が変化し、同じ実行が Release で約 30 秒あれば、Debug では 5 分を超える場合があります。コードが動作するたびに、常にトレーニングを -c Release で実行します。残りのスプロッドはコースの最後のパフォーマンス最適化ノートセクションでカバーされます。今、Linear と Softmax を Helpers.cs に追加します:
// --- Helpers.cs ---
namespace MicroGPT;
public static class Helpers {
/// /// 行列ベクトル掛け。重みの各行が入力と要素毎に掛け合わされ、単一の値に合計される。///
public static List Linear(List input, List> weights) => [.. weights.Select(row => Value.Dot(row, input))];
/// /// 生スコア(ロジット)を確率分布に変換する。///
public static List Softmax(List logits) {
double maxVal = logits.Max(v => v.Data);
var exponentials = logits.Select(v => (v - maxVal).Exp()).ToList();
var total = new Value(0);
foreach (Value? e in exponentials) {
total += e;
}
return [.. exponentials.Select(e => e / total)];
}
}
Linear の各要素は、入力への重み付け合計であり、重みには学習されたパラメータが含まれています。入力が 16 要素、重みが 64 行の 16 要素ごとに、出力は 64 要素です。これはニューラルネットワークがデータの次元性をどのように変化するのでしょうか。ドット積の後に偏項を加えはありません。生産モデルは通常 1 つを含みます(出力 = 重み * 入力 + 偏項)が、単純化のためにそれを省略します。LLaMAなどの一部の現代アーキテクチャも偏項を落とします。モデルから確率に変換される前に出る生の数字はロジットと呼ばれます。ML のどこかでその用語を見ます。それらはどんな値でもよい(正の値、負の値、大きい値、小さな値)であり、それら自体は多くの意味を意味しません。それらはまず確率分布に変換する必要があります、それが Softmax の場所です。Softmax はロジットのリストを受け取り、すべて [0, 1] にあり合計が 1 になる確率分布に変換します。数値安定性のために、exp を取る前に最大値を引きます。数学的には結果を変更しませんが(最大値は除算で消去される)、それがなければ、exp の大きな数は無限大にオーバーフローします。バックワードパスにも影響を与えません:すべてのロジットを同じ定数でシフトすることは比で消去されるので、Softmax を通る勾配は未シフトバージョンと同じになります。Linear と Softmax はどちらも Value オペレーション(加算、乗算、exp、除算)から完全に構築されているので、勾配はバックワードパスの間自動的にそれらを通過します。彼らは「凍結」数学ではありません。計算グラフの一部です、他の Value オペレーションのチェーンと同じように。Chapter5Exercise.cs を作成します:
// --- Chapter5Exercise.cs -
Original Content
What You'll Build Two helper functions that show up in nearly every layer of a neural network: Linear takes an input vector and a weight matrix, multiplies each row of weights element-by-element with the input, and sums each row into a single output value: input: [1, 2, 3] weights: [[0.1, 0.2, 0.3], row 0: 0.1*1 + 0.2*2 + 0.3*3 = 1.4 [0.4, 0.5, 0.6]] row 1: 0.4*1 + 0.5*2 + 0.6*3 = 3.2 output: [1.4, 3.2] Two rows of weights means two output values. This is how neural networks change the size of data as it flows through layers. Softmax takes a list of raw numbers and turns them into probabilities that add up to 1. For example, [2.0, 1.0, 0.1] becomes roughly [0.66, 0.24, 0.10]. The largest input gets the highest probability. They live in their own file because they're pure math utilities, independent of the model architecture. Chapters 1-2 (the Value class - our computation recorder). Linear needs a way to compute a dot product between two lists of Value objects. Add this method to Value.cs: // --- Value.cs (add inside the Value class) --- public static Value Dot(List a, List b) { var result = new Value(0); for (int i = 0; i < a.Count; i++) { result += a[i] * b[i]; } return result; } Debug vs Release matters from here on. Each += allocates a fresh Value, and a typical training step does tens of thousands of them. In Debug mode the JIT skips inlining and the GC churns, so the same run that takes ~30 seconds in Release can take 5+ minutes in Debug. Once the code is working, always run training with -c Release. The Performance Optimisation Notes section at the end of the course covers the rest of the speedups. Now add Linear and Softmax to Helpers.cs: // --- Helpers.cs --- namespace MicroGPT; public static class Helpers { /// /// Matrix-vector multiply. Each row of weights is multiplied element-by-element /// with input and summed into a single value. /// public static List Linear(List input, List> weights) => [.. weights.Select(row => Value.Dot(row, input))]; /// /// Converts raw scores (logits) into a probability distribution. /// public static List Softmax(List logits) { double maxVal = logits.Max(v => v.Data); var exponentials = logits.Select(v => (v - maxVal).Exp()).ToList(); var total = new Value(0); foreach (Value? e in exponentials) { total += e; } return [.. exponentials.Select(e => e / total)]; } } Each element of Linear's output is a weighted sum of input, where weights contains the learned parameters. If input has 16 elements and weights has 64 rows of 16 elements each, the output has 64 elements. This is how neural networks change the dimensionality of data. Notice there's no bias term added after the dot product. Production models typically include one (output = weights * input + bias), but we omit it for simplicity. Some modern architectures like LLaMA also drop biases. The raw numbers that come out of a model before they're turned into probabilities are called logits. You'll see the term everywhere in ML. They can be any value (positive, negative, large, small), and on their own they don't mean much. They need to be converted into probabilities first, which is where Softmax comes in. Softmax takes a list of logits and turns them into a probability distribution where all values are in [0, 1] and sum to 1. We subtract the max value before taking exp for numerical stability. Mathematically it doesn't change the result (the max cancels out in the division), but without it, exp of large numbers can overflow to infinity. The backward pass is unaffected too: shifting every logit by the same constant cancels in the ratio, so the gradients through Softmax come out identical to the unshifted version. Because both Linear and Softmax are built entirely from Value operations (add, multiply, exp, divide), gradients flow through them automatically during the backward pass. They aren't "frozen" math. They're part of the computation graph, just like any other chain of Value operations. Create Chapter5Exercise.cs: // --- Chapter5Exercise.cs --- using static MicroGPT.Helpers; namespace MicroGPT; public static class Chapter5Exercise { public static void Run() { // Test Linear: a 2x3 weight matrix times a length-3 input vector var input = new List { new(1.0), new(2.0), new(3.0) }; var weights = new List> { new() { new(0.1), new(0.2), new(0.3) }, // row 0: 0.1*1 + 0.2*2 + 0.3*3 = 1.4 new() { new(0.4), new(0.5), new(0.6) }, // row 1: 0.4*1 + 0.5*2 + 0.6*3 = 3.2 }; List output = Linear(input, weights); Console.WriteLine("--- Linear ---"); Console.WriteLine("Expected: 1.4 3.2"); Console.Write("Got: "); foreach (Value v in output) { Console.Write($"{v.Data:F1} "); } Console.WriteLine(); // Test Softmax: converts raw logits into probabilities that sum to 1 var logits = new List { new(2.0), new(1.0), new(0.1) }; List probabilities = Softmax(logits); Console.WriteLine("--- Softmax ---"); Console.WriteLine( "Expected: 0.659 0.242 0.099 (sum to 1.0, largest logit gets highest prob)" ); Console.Write("Got: "); foreach (Value p in probabilities) { Console.Write($"{p.Data:F3} "); } Console.WriteLine(); } } Uncomment the Chapter 5 case in the dispatcher in Program.cs: case "ch5": Chapter5Exercise.Run(); break; Then run it: dotnet run -- ch5 "Why not just divide each logit by the sum of all logits?" Because logits can be negative, and a probability distribution needs all non-negative values. The exp function makes sure everything is positive before normalising.