第13章 線形だけでは足りない理由

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

第12章の最後に、宿題を残しました。768 \rightarrow 3072 \rightarrow 768 という構造が、線形変換だけでは意味を持たない、という話です。第13章では、この理由を正面から扱います。この連載は、なぜディープラーニングに線形代数が必要なのかがテーマですが、この章だけは、線形代数の限界を扱います。線形変換だけを何層重ねても、一層と同じ表現力しか持ちません。この事実を理解することで、なぜ活性化関数が必要なのか、なぜディープラーニングがディープであることに意味があるのかが、明確になります。線形代数にできないことを知ることは、線形代数の役割を正確に把握することでもあります。

線形変換を重ねても一層と等価になる

数式で確認する

二層のネットワークを考えます。活性化関数はありません。

\mathbf{h} = W_1\mathbf{x}

\mathbf{y} = W_2\mathbf{h}

これを一つの式にまとめます。

\mathbf{y} = W_2(W_1\mathbf{x}) = (W_2W_1)\mathbf{x}

W_2W_1 は、行列の積です。計算すれば、一つの行列になります。

これを W と置けば、次のようになります。

\mathbf{y} = W\mathbf{x}

一層のネットワークと、まったく同じ形です。

何層でも同じ

三層でも、十層でも、百層でも同じです。

\mathbf{y} = W_{100}W_{99}\cdots W_2W_1\mathbf{x}

すべての行列を掛け合わせれば、一つの行列になります。

つまり、活性化関数のない百層のネットワークは、一層のネットワークと同じ表現力しか持ちません。

計算資源とパラメータを百倍使って、一層と同じことしかできない。これでは、層を重ねる意味がありません。

第12章の話とのつながり

第12章で、768 \rightarrow 3072 \rightarrow 768 という構造のパラメータ数を計算しました。

拡大の行列が約236万個、縮小の行列も約236万個、合計約472万個です。

一方、768次元から768次元への一つの行列であれば、約59万個で済みます。

線形変換だけであれば、8倍のパラメータを使って、8分の1のパラメータで実現できることをやっていることになります。

明らかに無駄です。この構造に意味があるのは、間に非線形な処理が挟まるからです。

線形変換にできないこと

では、線形変換には何ができないのでしょうか。

制約1:原点は必ず原点に移る

線形変換では、次が成り立ちます。

A\mathbf{0} = \mathbf{0}

ゼロベクトルを入力すると、必ずゼロベクトルが出ます。

なお、ニューラルネットワークではバイアスを加えるため、この制約は緩和されます。

\mathbf{y} = W\mathbf{x} + \mathbf{b}

入力がゼロでも、バイアスの分だけ出力が生じます。

このように、線形変換に平行移動を加えたものを、アフィン変換と呼びます。

ただし、アフィン変換を重ねても、やはりアフィン変換にしかなりません。

\mathbf{y} = W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = W_2W_1\mathbf{x} + (W_2\mathbf{b}_1 + \mathbf{b}_2)

W_2W_1 を新しい行列、W_2\mathbf{b}_1 + \mathbf{b}_2 を新しいバイアスと考えれば、一層のアフィン変換と等価です。

バイアスを加えても、根本的な問題は解決しません。

制約2:直線は直線のまま

線形変換では、直線が直線に移ります。曲がることはありません。

したがって、曲線的な境界を表現できません。

制約3:曲げられない、折れない

第11章で扱った変換を思い出してください。拡大、回転、せん断、射影。いずれも、空間をまっすぐ変形させるだけでした。

紙を伸ばしたり、回したり、傾けたりはできますが、折り曲げることはできません。

XOR問題という古典的な例

線形変換の限界を示す、最も有名な例を紹介します。

問題の設定

二つの入力があり、それぞれ0か1の値を取ります。出力は、次のように決まります。

入力1入力2出力
000
011
101
110

二つの入力が異なるときだけ、出力が1になります。これを、排他的論理和、あるいはXORと呼びます。

図で考える

平面上に、四つの点を配置します。

(0, 0)(1, 1) は、出力0です。

(0, 1)(1, 0) は、出力1です。

出力0の点は、左下と右上にあります。出力1の点は、左上と右下にあります。

対角線上に、同じ種類の点が配置されている状態です。

直線1本では分けられない

この四つの点を、1本の直線で二つのグループに分けることはできません。

どのように直線を引いても、必ず一方のグループに他方の点が混ざります。

紙に描いて試してみてください。何本引いても、分けられません。

線形変換では解けない

活性化関数のないニューラルネットワークは、結局のところ、一つのアフィン変換です。

y = w_1x_1 + w_2x_2 + b

この出力が正か負かで分類するとすれば、境界は次の式で表される直線です。

w_1x_1 + w_2x_2 + b = 0

直線1本しか引けないため、XOR問題は解けません。

歴史的な影響

この問題は、1969年に指摘され、ニューラルネットワーク研究が一時的に停滞する原因の一つになったと言われています。

解決策は、層を重ねることと、非線形な処理を導入することでした。この二つが揃って初めて、複雑な境界が表現できるようになります。

活性化関数という解決策

何をするのか

層と層の間に、非線形な関数を挟みます。

\mathbf{h} = f(W_1\mathbf{x} + \mathbf{b}_1)

\mathbf{y} = W_2\mathbf{h} + \mathbf{b}_2

この f が、活性化関数です。

なぜこれで解決するのか

f が非線形であれば、次の式変形ができません。

\mathbf{y} = W_2 f(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2

f が間にあるため、W_2W_1 を掛け合わせて一つにまとめることができないのです。

したがって、層を重ねるごとに、表現できる関数が本当に豊かになります。

代表的な活性化関数

いくつか紹介します。

ReLUは、次の関数です。

ReLU(x) = \max(0, x)

入力が正であればそのまま通し、負であれば0にします。

グラフを描くと、原点で折れ曲がった形になります。この折れ曲がりが、非線形性の正体です。

シグモイド関数は、次の関数です。

\sigma(x) = \frac{1}{1 + e^{-x}}

出力が0から1の範囲に収まる、なめらかなS字カーブを描きます。

GELUは、ReLUをなめらかにしたような関数で、現在の言語モデルで広く使われています。

折れ線で曲線を近似する

ReLUを例に、非線形性がどう効くかを考えます。

ReLUは、原点で一度折れ曲がるだけの、単純な関数です。

しかし、多数のReLUを組み合わせると、折れ曲がる位置と角度を自在に調整できます。

十分な数を組み合わせれば、任意の曲線を、折れ線で好きなだけ精密に近似できます。

これが、ニューラルネットワークの表現力の源泉です。

普遍近似定理

数学的には、次のことが証明されています。

十分な数のニューロンを持つ一層の隠れ層と、適切な活性化関数があれば、任意の連続関数を、任意の精度で近似できる。

これを、普遍近似定理と呼びます。

ただし、注意が必要です。この定理は近似できると言っているだけで、効率的に学習できるとは言っていません。

必要なニューロンの数が現実的でない場合もあります。実際には、層を深くするほうが、少ないパラメータで複雑な関数を表現できることが多い、と経験的に知られています。

XOR問題を解いてみる

活性化関数があれば、XOR問題が解けることを確認します。

構成

入力2次元、隠れ層2次元、出力1次元のネットワークを考えます。

隠れ層にReLUを使います。

重みの設定

次のように設定します。

W_1 = \begin{pmatrix} 1 & 1 \\ 1 & 1 \end{pmatrix}, \quad \mathbf{b}_1 = \begin{pmatrix} 0 \\ -1 \end{pmatrix}

W_2 = \begin{pmatrix} 1 & -2 \end{pmatrix}, \quad b_2 = 0

計算を追う

入力 [0, 0] の場合。

W_1\mathbf{x} + \mathbf{b}_1 = [0, 0] + [0, -1] = [0, -1]

ReLUを適用すると、[0, 0] になります。

W_2 \cdot [0, 0] = 0

出力は0です。正解です。

入力 [0, 1] の場合。

W_1\mathbf{x} + \mathbf{b}_1 = [1, 1] + [0, -1] = [1, 0]

ReLUを適用すると、[1, 0] のままです。

W_2 \cdot [1, 0] = 1

出力は1です。正解です。

入力 [1, 0] の場合も、対称性から同じく1になります。

入力 [1, 1] の場合。

W_1\mathbf{x} + \mathbf{b}_1 = [2, 2] + [0, -1] = [2, 1]

ReLUを適用すると、[2, 1] のままです。

W_2 \cdot [2, 1] = 1 \times 2 + (-2) \times 1 = 0

出力は0です。正解です。

すべて正しく分類できました。

何が起きたのか

隠れ層の1番目のニューロンは、入力の合計を計算しています。

隠れ層の2番目のニューロンは、入力の合計から1を引いた値を、ReLUで0以上に制限しています。

入力が両方1のときだけ、2番目のニューロンが反応します。この反応を、出力層で差し引くことで、XORが実現されています。

ReLUによる負の値を0にするという非線形な操作が、この仕組みの鍵です。

線形代数の役割は変わらない

ここまで、線形変換の限界を扱ってきました。

では、線形代数は重要ではないのでしょうか。

そうではありません。

計算の大半は依然として線形代数

活性化関数の計算は、要素ごとの単純な処理です。計算量としては、ごくわずかです。

計算の大半は、行列積が占めています。第9章で計算したとおり、1つの層で数千万回の掛け算が行われます。

活性化関数は、その間に挟まる、軽い処理にすぎません。

役割分担

線形変換が、情報を混ぜ合わせ、変換します。

活性化関数が、非線形性を与えます。

この二つが交互に繰り返されることで、複雑な関数が表現されます。

どちらが欠けても、成立しません。

誤差逆伝播も線形代数

第15章で扱いますが、学習時の勾配計算も、行列演算で記述されます。

活性化関数の微分は、要素ごとの掛け算として表現され、それ以外は行列の積です。

つまり、順方向の計算も、逆方向の計算も、線形代数が主役です。

Pythonで確認する

import numpy as np

print("=== 線形変換を重ねると一つの行列になる ===")
np.random.seed(0)

W1 = np.random.randn(64, 32)
W2 = np.random.randn(128, 64)
W3 = np.random.randn(16, 128)
x = np.random.randn(32)

step_by_step = W3 @ (W2 @ (W1 @ x))
W_combined = W3 @ W2 @ W1
y_combined = W_combined @ x

print(f"3層を順に通した結果(最初の5要素): {step_by_step[:5].round(6)}")
print(f"合成行列で計算した結果(最初の5要素): {y_combined[:5].round(6)}")
print(f"一致するか: {np.allclose(step_by_step, y_combined)}")
print(f"\n合成行列の形: {W_combined.shape}")
print(f"3層分のパラメータ数: {W1.size + W2.size + W3.size:,}")
print(f"合成行列のパラメータ数: {W_combined.size:,}")
print("同じ計算を、はるかに少ないパラメータで実現できてしまいます。")

print("\n=== アフィン変換も同様 ===")
b1 = np.random.randn(64)
b2 = np.random.randn(128)

h1 = W1 @ x + b1
y_affine = W2 @ h1 + b2

W_comb = W2 @ W1
b_comb = W2 @ b1 + b2
y_comb = W_comb @ x + b_comb

print(f"2層のアフィン変換: {y_affine[:5].round(6)}")
print(f"合成した1層: {y_comb[:5].round(6)}")
print(f"一致するか: {np.allclose(y_affine, y_comb)}")

print("\n=== XOR問題 ===")
X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
], dtype=float)
y_true = np.array([0, 1, 1, 0])

print("入力と正解:")
for i in range(4):
    print(f"  {X[i]} → {y_true[i]}")

print("\n=== 線形モデルでは解けない ===")
best_acc = 0
best_params = None
for w1 in np.arange(-2, 2.1, 0.5):
    for w2 in np.arange(-2, 2.1, 0.5):
        for b in np.arange(-2, 2.1, 0.5):
            pred = (X[:, 0] * w1 + X[:, 1] * w2 + b > 0).astype(int)
            acc = (pred == y_true).mean()
            if acc > best_acc:
                best_acc = acc
                best_params = (w1, w2, b)

print(f"総当たりで探した最良の線形分類器: w1={best_params[0]}, w2={best_params[1]}, b={best_params[2]}")
print(f"正解率: {best_acc:.2%}")
print("どんな直線を引いても、4点すべてを正しく分類できません。")

print("\n=== 活性化関数を入れると解ける ===")
def relu(x):
    return np.maximum(0, x)

W1_xor = np.array([[1.0, 1.0],
                   [1.0, 1.0]])
b1_xor = np.array([0.0, -1.0])
W2_xor = np.array([[1.0, -2.0]])
b2_xor = np.array([0.0])

print(f"\n{'入力':>8} | {'W1x+b1':>12} | {'ReLU後':>12} | {'出力':>6} | {'正解':>6}")
print("-" * 56)
for i in range(4):
    x_i = X[i]
    z1 = W1_xor @ x_i + b1_xor
    h = relu(z1)
    out = (W2_xor @ h + b2_xor)[0]
    print(f"{str(x_i):>8} | {str(z1):>12} | {str(h):>12} | {out:>6.1f} | {y_true[i]:>6}")

print("\nすべて正しく分類できました。")

print("\n=== 活性化関数がないと解けないことを確認 ===")
print(f"{'入力':>8} | {'活性化なしの出力':>18} | {'正解':>6}")
print("-" * 38)
for i in range(4):
    x_i = X[i]
    z1 = W1_xor @ x_i + b1_xor
    out_no_act = (W2_xor @ z1 + b2_xor)[0]
    print(f"{str(x_i):>8} | {out_no_act:>18.1f} | {y_true[i]:>6}")
print("同じ重みでも、活性化関数がないと正しく分類できません。")

print("\n=== ReLUの組み合わせで曲線を近似する ===")
x_test = np.linspace(-3, 3, 100)
target = np.sin(x_test)

print("ReLUの個数と、sin関数への近似誤差:")
for n in [2, 5, 10, 20, 50]:
    breakpoints = np.linspace(-3, 3, n)
    A = np.column_stack([relu(x_test - bp) for bp in breakpoints] + [np.ones_like(x_test)])
    coeffs, _, _, _ = np.linalg.lstsq(A, target, rcond=None)
    approx = A @ coeffs
    error = np.abs(target - approx).mean()
    print(f"  ReLU {n:>2}個: 平均誤差 {error:.6f}")

print("\nReLUの個数を増やすほど、曲線を精密に近似できます。")

print("\n=== 計算量の比較:行列積と活性化関数 ===")
batch, d_in, d_out = 32, 768, 3072
matmul_ops = batch * d_in * d_out
activation_ops = batch * d_out

print(f"行列積の演算回数: {matmul_ops:,}")
print(f"活性化関数の演算回数: {activation_ops:,}")
print(f"比率: 活性化関数は行列積の {activation_ops/matmul_ops:.4%}")
print("計算量の大半は、依然として行列積が占めています。")

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

つまずき1:バイアスを加えれば非線形になると思う

バイアスを加えても、アフィン変換になるだけです。

アフィン変換を重ねても、やはりアフィン変換です。層を重ねる意味は生じません。

非線形性を与えるのは、活性化関数だけです。

つまずき2:ReLUがほとんど線形に見えて混乱する

ReLUのグラフは、正の領域では直線、負の領域では0という、単純な形をしています。

これのどこが非線形なのか、と感じるかもしれません。

しかし、原点で折れ曲がっている一点が、決定的です。この折れ目があるため、線形変換の定義を満たしません。

そして、折れ目の位置を変えたReLUを多数組み合わせれば、任意の折れ線が作れます。折れ線で、任意の曲線を近似できます。

つまずき3:線形代数が不要だと考える

この章で線形変換の限界を扱ったため、線形代数の重要性が下がったように感じるかもしれません。

しかし、計算量の大半は、依然として行列積です。演習で示したとおり、活性化関数の計算量は、行列積の0.1パーセント未満です。

線形変換が主役であり、活性化関数はその間に挟まる、軽いが不可欠な要素である。この関係を押さえてください。

演習

演習1:合成を確認する

W_1 = \begin{pmatrix} 2 & 0 \\ 1 & 1 \end{pmatrix}W_2 = \begin{pmatrix} 1 & 1 \\ 0 & 2 \end{pmatrix} とします。

\mathbf{x} = [1, 2] について、次を計算してください。

W_2(W_1\mathbf{x})

(W_2W_1)\mathbf{x}

両者が一致することを確認してください。

演習2:ANDとORは線形で解けるか

XORは線形では解けませんでした。では、次の二つはどうでしょうか。

AND、両方が1のときだけ出力1。

OR、どちらかが1なら出力1。

それぞれについて、直線1本で分離できるかを、図を描いて考えてください。

分離できる場合は、w_1x_1 + w_2x_2 + b の形で、実際の重みとバイアスの例を挙げてください。

演習3:XORの計算を追う

本文で示した重みを使って、入力 [1, 0] の場合の計算を、手で追ってください。

W_1\mathbf{x} + \mathbf{b}_1 、ReLU適用後、最終出力の順に求めてください。

演習4:活性化関数の効果を実験する

import numpy as np

np.random.seed(0)

def relu(x):
    return np.maximum(0, x)

def make_network(n_hidden, activation=None):
    W1 = np.random.randn(n_hidden, 1) * 1.0
    b1 = np.random.randn(n_hidden) * 1.0
    W2 = np.random.randn(1, n_hidden) * 0.5
    b2 = np.random.randn(1) * 0.1
    
    def forward(x):
        z1 = W1 @ x.reshape(1, -1) + b1.reshape(-1, 1)
        h = activation(z1) if activation else z1
        return (W2 @ h + b2).flatten()
    
    return forward

x = np.linspace(-3, 3, 50)

print("=== 活性化関数なしのネットワークの出力 ===")
net_linear = make_network(20, activation=None)
out_linear = net_linear(x)
second_diff = np.diff(out_linear, n=2)
print(f"2階差分の最大絶対値: {np.abs(second_diff).max():.10f}")
print("ほぼ0であり、出力が直線であることが分かります。")

print("\n=== ReLUを入れたネットワークの出力 ===")
net_relu = make_network(20, activation=relu)
out_relu = net_relu(x)
second_diff_relu = np.diff(out_relu, n=2)
print(f"2階差分の最大絶対値: {np.abs(second_diff_relu).max():.6f}")
print("0でない値があり、出力が曲がっていることが分かります。")

print("\n=== 出力の形を数値で比較 ===")
print(f"{'x':>7} | {'活性化なし':>12} | {'ReLUあり':>12}")
print("-" * 36)
for i in range(0, 50, 7):
    print(f"{x[i]:>7.2f} | {out_linear[i]:>12.4f} | {out_relu[i]:>12.4f}")

演習の解答例と解説

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

まず W_1\mathbf{x} を計算します。

\begin{pmatrix} 2 & 0 \\ 1 & 1 \end{pmatrix}\begin{pmatrix} 1 \\ 2 \end{pmatrix} = \begin{pmatrix} 2 \\ 3 \end{pmatrix}

次に W_2 を掛けます。

\begin{pmatrix} 1 & 1 \\ 0 & 2 \end{pmatrix}\begin{pmatrix} 2 \\ 3 \end{pmatrix} = \begin{pmatrix} 5 \\ 6 \end{pmatrix}

一方、W_2W_1 を先に計算します。

\begin{pmatrix} 1 & 1 \\ 0 & 2 \end{pmatrix}\begin{pmatrix} 2 & 0 \\ 1 & 1 \end{pmatrix} = \begin{pmatrix} 3 & 1 \\ 2 & 2 \end{pmatrix}

これを \mathbf{x} に掛けます。

\begin{pmatrix} 3 & 1 \\ 2 & 2 \end{pmatrix}\begin{pmatrix} 1 \\ 2 \end{pmatrix} = \begin{pmatrix} 5 \\ 6 \end{pmatrix}

一致しました。

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

ANDについて。出力1になるのは (1, 1) だけです。この点だけを他から分離すればよいため、右上を切り取る直線が引けます。

例として、x_1 + x_2 - 1.5 が挙げられます。(1, 1) では0.5でプラス、他の三点ではマイナスになります。

ORについて。出力0になるのは (0, 0) だけです。左下を切り取る直線が引けます。

例として、x_1 + x_2 - 0.5 が挙げられます。(0, 0) ではマイナス0.5、他の三点ではプラスになります。

XORとの違いは、分離すべき点の配置です。ANDとORでは1点だけを切り離せばよいのに対し、XORでは対角線上の2点ずつを分ける必要があります。この配置が、直線1本では不可能なのです。

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

W_1\mathbf{x} + \mathbf{b}_1 を計算します。

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

バイアスを加えます。

\begin{pmatrix} 1 \\ 1 \end{pmatrix} + \begin{pmatrix} 0 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 \\ 0 \end{pmatrix}

ReLUを適用します。両方とも0以上なので、変わりません。

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

最終出力を計算します。

\begin{pmatrix} 1 & -2 \end{pmatrix}\begin{pmatrix} 1 \\ 0 \end{pmatrix} = 1 \times 1 + (-2) \times 0 = 1

出力は1です。正解と一致します。

演習4では、活性化関数のないネットワークの出力が、2階差分がほぼ0、つまり直線になることが確認できます。

一方、ReLUを入れると、2階差分が0でない値を取り、出力が曲がっていることが分かります。

隠れ層を20個持つネットワークであっても、活性化関数がなければ直線しか表現できない。この事実が、数値として確認できます。

講師向けの補足

この章では、XOR問題を必ず図で示してください。

四つの点を平面に描き、受講者に1本の直線で分けてみてくださいと促すのが効果的です。実際に何本か引いてみて、どうしても分けられないことを体験してもらいます。

その後、活性化関数を入れると解けることを、計算を追いながら示します。解けなかったものが解けるようになったという体験が、活性化関数の必要性を強く印象づけます。

また、線形代数の限界を扱う章だが、線形代数が不要になるわけではないという点を、最後に明確にしてください。計算量の比較を示すと、説得力があります。

普遍近似定理については、深入りしないことを推奨します。理論上は一層でも十分だが、実際には深いほうが効率的、という程度の紹介にとどめてください。

理解度を確認する問い

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

活性化関数のない百層のネットワークが、一層と等価になる理由を、数式で説明してみてください。

バイアスを加えても問題が解決しないのは、なぜでしょうか。

XOR問題が線形分類器で解けないのは、なぜでしょうか。ANDやORとの違いに触れて説明してください。

ReLUのどこが非線形なのでしょうか。

活性化関数を導入しても、線形代数が依然として重要である理由を説明してください。

まとめ

線形変換だけを何層重ねても、行列を掛け合わせることで一つの行列にまとめられるため、一層と同じ表現力しか持ちません。バイアスを加えてアフィン変換にしても、同様です。

線形変換には、原点が原点に移り、直線が直線のまま保たれるという制約があります。曲がった境界を表現できないため、XOR問題のように、対角線上に同じ種類の点が配置された問題は解けません。

解決策は、層と層の間に非線形な関数、すなわち活性化関数を挟むことです。活性化関数があると、行列を掛け合わせて一つにまとめることができなくなるため、層を重ねることに意味が生じます。

ReLUは、原点で折れ曲がるだけの単純な関数ですが、この折れ目が非線形性の正体です。折れる位置を変えたReLUを多数組み合わせれば、任意の曲線を折れ線で精密に近似できます。

ただし、活性化関数の導入によって、線形代数の重要性が下がるわけではありません。計算量の大半は依然として行列積が占めており、活性化関数はその間に挟まる軽い処理です。線形変換が情報を混ぜ合わせて変換し、活性化関数が非線形性を与える。この二つが交互に繰り返されることで、複雑な関数が表現されます。

次の第14章からは、第IV部に入ります。ここまでは、入力から出力へ向かう順方向の計算を扱ってきました。第14章では、学習の仕組みに現れる線形代数として、勾配ベクトルを扱います。パラメータをどちらにどれだけ動かすべきかという情報が、ベクトルとして表現されることを確認します。

第14章に進む前に、演習4のコードを実行し、活性化関数の有無で出力の形がどう変わるかを確認しておいてください。活性化関数がなければ直線しか作れないという事実を数値で見た経験が、以降の理解を支えます。

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

投稿者プロフィール

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

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